# coding=utf-8

# 用于公共接口和方法的封装
import json
import allure
from api.client import HttpClient
from jsonpath import jsonpath
from jsonpath_ng.ext import parse
import gzip
import io
import base64

class Common_Api(HttpClient):
    def parse_params(self, process_params, param_name, allow_return_none=0):
        """
        传入yml参数list和所需的参数名，获取对应的参数值。allow_return_none=0默认不允许返回None，传入其他值则可以返回None
        :param process_params:
        :param param_name: 目前有：process_name、form_values、branch_names、step_cc_list
        :param allow_return_none: 允许返回None，默认不允许。也就是yml里面的参数，必须正确且存在。但是form_values，branch_names，step_cc_list可以不存在，所以需要返回None。
        :return:
        """
        # 参数解析，后续加入参数，再次解析参数值
        try:
            return process_params[param_name]
        except KeyError:
            if allow_return_none != 0:
                return None
            else:
                assert False, f'yml文件中，参数【{param_name}】不存在，请检查！'

    # 通过用户搜索用户信息
    def get_users_by_name(self,name):
        """
        通过用户名称或者用户code，查询用户信息
        :param name: 可以是名称，也可以是code
        :return:
        """
        url = f'/organization/ou/get-users-by-name?filter_disable=1&pi=1&ps=30&package_id=undefined&name={name}&_=1623814283363'
        resp = self.send(url,method='get')
        return resp

    def get_user_by_id(self,guids:list):
        """
        通过一组用户guid，获取用户信息
        :param guids: 用户guid列表
        :return:
        """
        url = '/organization/ou/get-user-by-id'
        body = {"id":guids,"package_id":""}
        resp = self.send(url, payload=body, method='post', X_CSRF_Token=self.csrf_token)
        return resp

    def get_default_cc_list(self,guids):
        """
        获取默认抄送人信息
        :param guids:
        :return:
        """
        resp = self.get_user_by_id(guids)
        cc_list = []
        for id in guids:
            auditor_name = self.jsonpath_value(resp.json(),f'$.data.data[?(@.id=="{id}")].name')
            station_name = self.jsonpath_value(resp.json(),
                                               f'$.data.data[?(@.id=="{id}")].org_settings[?(@.is_default=="1" or @.is_default==1)].name_path')
            cc_info = {'auditor_name':auditor_name,'auditor_id':id,'station_name':station_name}
            cc_list.append(cc_info)
        return cc_list

    def get_cc_list_by_name(self, process_params=None,cc_list=None):
        """
        通过姓名，获取抄送人信息列表,process_params：用例的param参数，cc_list=['张三','李四']，两个入参必须传一个，如果两个都传，取cc_list
        :param process_params:
        :cc_list:
        :return:
        """
        step_cc_list = []
        cc_list1=[]
        if process_params!=None:
            cc_list1 = self.parse_params(process_params, 'step_cc_list', 1)
        if cc_list !=None:
            cc_list1=cc_list
        # 如果抄送列表有配置人，则进行抄送人参数组装
        if cc_list1:
            for cc in cc_list1:
                resp = self.get_users_by_name(cc)
                resp_json = json.loads(resp.text)
                if resp.json()['data']['total']=="0":
                    assert False,f'用户：【{cc}】不存在，请检查yml数据源'
                # 获取抄送列表入参信息并组装成json
                auditor_name = self.jsonpath_value(resp_json, '$.data.data[0].name')
                auditor_id = self.jsonpath_value(resp_json, '$.data.data[0].id')
                auditor_code = self.jsonpath_value(resp_json, '$.data.data[0].code')
                station_name = self.jsonpath_value(resp_json,
                                                   '$.data.data[0].org_settings[?(@.is_default=="1" or @.is_default==1)].name_path')
                station_guid = self.jsonpath_value(resp_json,
                                                   '$.data.data[0].org_settings[?(@.is_default=="1" or @.is_default==1)].org_id')
                auditor = {'auditor_name': auditor_name, 'auditor_id': auditor_id, 'auditor_code': auditor_code,
                           'station_name': station_name, 'station_guid': station_guid}
                # 将多个抄送人信息，组装成入参list
                step_cc_list.append(auditor)
        return step_cc_list

    @allure.step('获取业务单据关联的发起人信息及默认抄送信息')
    def get_user_orgs(self, **kwargs):
        """
        获取业务单据关联的发起人信息及默认抄送信息。
        :param kwargs: 目前只需要token='xxx'。
        :return:
        """
        csrf_token = kwargs['token']
        json_body = {}
        json_body.update(self.params_json)
        json_body.pop('flow_id')
        json_body.pop('package_id')
        json_body.pop('form_id')
        # 以下key是发起没有的，直接跳过
        try:
            json_body.pop('node_id')
            json_body.pop('data_control')
            json_body.pop('attachment_control')
            json_body.pop('ParentProcess')
        except KeyError:
            pass
        json_body.pop('auditor_name')
        json_body.pop('CurUserGUID')
        json_body.pop('CurUserName')
        json_body.pop('proc_initiater')
        json_body.pop('initiater')
        json_body.pop('sign_id')
        json_body.pop('OpinionBackFill')
        json_body.pop('IsSubProc')
        json_body.pop('name_rule')
        initiater_org_id = self.jsonpath_value(self.params_json, '$.proc_initiater.initiater_org_id')
        initiater_id = self.jsonpath_value(self.params_json, '$.proc_initiater.initiater_id')
        form_data = {}
        main = self.jsonpath_value(self.tpldata_json, '$.form.form_data.main')
        children = self.jsonpath_value(self.tpldata_json, '$.form.form_data.children')
        approve_buttons = self.jsonpath_value(self.tpldata_json, '$.ApproveButtons[*].OperationCode')
        # 判断审批页面按钮，如果按钮存在Initiate，说明是发起，否则是审批
        if "Initiate" in approve_buttons:
            mode = "initiate"
        else:
            mode = "approval"
        form_data.update(main=main, children=children)
        json_body.update({"canAgent": None, "form_data": form_data, "initiater_org_id": initiater_org_id,
                          "initiater_id": initiater_id, "mode": mode})
        print(f"get_user_orgs={json_body}")
        url = '/flowinstance/process/ajax-get-user-orgs'
        # url = '/flowinstance/process/ajax-get-user-orgs-v2?search_mode=2'
        resp = self.send(url, method='post', payload=json_body, X_CSRF_Token=csrf_token)
        self.user_orgs_resp = resp
        return resp

    @allure.step('选择步骤责任人')
    def update_steps_by_auditor(self,actual_steps,choose_step_auditor):
        """
        传入原始的步骤信息json和需要选择的步骤责任人，刷新新的步骤信息
        :param actual_steps: 发起确认路径时或者审批确认路径时，返回的步骤信息列表
        :param choose_step_auditor: 需要选择的步骤信息，choose_step_auditor = [{step_name: '单人审批允许跳过，抄送发起人', auditor: ['李乾芳']},...]
        :return: 选择责任人后，更新后的步骤信息列表
        """
        # 获取到发起参数的字典，如果存在step_auditor，则进行责任人刷新
        # choose_step_auditor = [{step_name: '单人审批允许跳过，抄送发起人', auditor: ['李乾芳']},...]
        if choose_step_auditor:
            # 根据步骤名，刷新步骤对应的责任人
            for step in choose_step_auditor:
                auditor_list = [] # 进入一个步骤，初始一个需要更新的步骤责任人列表
                step_name = step['step_name']
                step_auditor = step['auditor']
                # 标准的jsonpath语法：     $.[?(@.name == "多人串行审批不允许调整不允许跳过")].stepAttrs.multi_type
                # jsonpath-ng中parse语法：$[?name = "多人串行审批不允许调整不允许跳过"].stepAttrs.multi_type
                if jsonpath(actual_steps, f'$.[?(@.name == "{step_name}")].stepAttrs.multi_type'):
                    multi_type = self.jsonpath_value(actual_steps, f'$.[?(@.name == "{step_name}")].stepAttrs.multi_type')
                else:
                    print(f'【{step_name}】步骤不存在，建议修改yml数据')
                    break
                # 如果步骤要选择多个责任人，进行多责任匹配和更新步骤责任人信息
                for auditor in step_auditor:
                    # 如果需要选择人的步骤是否节点的人为空，也就是步骤路径的责任为空，不允许跳过，允许调整责任人的场景。
                    if jsonpath(actual_steps,f'$.[?(@.name=="{step_name}")].nodes')[0]==[]:
                        # 通过请求获取用户组织信息
                        user_orgs = self.get_users_by_name(auditor)
                        user_default = self.jsonpath_value(user_orgs.json(),
                                                           '$.data.data[0].org_settings[?(@.is_default=="1" or @.is_default==1)]')
                        org_id = user_default['org_id']
                        name_path = user_default['name_path']
                        # Logger.info(f'用户【{auditor}】默认组织：【{name_path}】，对应组织GUID：【{org_id}】')
                        # 用户基本信息
                        user_base_info = self.jsonpath_value(user_orgs.json(), '$.data.data[0]')
                        user_name = user_base_info['name']
                        user_guid = user_base_info['id']
                        user_code = user_base_info['code']
                        auditor_info = {"auditor_id": user_guid,"auditor_name": user_name,"station_name": name_path,"station_guid": org_id,"auditor_code": user_code}
                    else:   # 否则从获取确认路径中的步骤责任中选择对应的人
                        # f'$.[?(@.name=="多人串行审批不允许调整不允许跳过")].nodes[?(@.auditor_name=="系统管理员")]'
                        auditor_info = self.jsonpath_value(actual_steps, f'$.[?(@.name=="{step_name}")].nodes[?(@.auditor_name=="{auditor}")]')
                    # 获取需要选中的责任人信息json
                    if multi_type == 'Single':
                        # 如果multi_type是单人步骤，选人则需要增加"Checked": 1
                        auditor_info['Checked']=1
                    # 将选中的责任人信息json，组装成责任人列表
                    auditor_list.append(auditor_info)
                # 更新选中的责任人信息参数到对应步骤json中
                parse(f'$[?(name="{step_name}")].nodes').update(actual_steps,auditor_list)
                # jsonpath(actual_steps,f'$.[?(@.name=="{step_name}")]',{"nodes":auditor_list})
        return actual_steps

    @allure.step('选择分支步骤')
    def choose_branch_step(self, resp, params_data, branch_name):
        """
        传入发起/提交审批的body，删掉部分参数，然后组合发起/提交审批确认路径响应的actuals，进行改造，得到choose_branch_step需要的入参
        :param resp: 第一次审批同意的响应
        :param params_data: 审批同意的body
        :param branch_name: 待选择的分支名称
        :return:
        """
        choose_body={}
        choose_body.update(params_data)
        choose_body.pop('step_cc_list')
        choose_body.pop('opinion')
        choose_body.pop('manual_execution')
        try:
            # 发起和审批body的key有差异，以下是审批特有的key
            choose_body.pop('actuals')
            choose_body.pop('branch_choose')
            choose_body.pop('archived_directory_id')
            choose_body.pop('voted_status')
            choose_body.pop('_prc_name_')
        except KeyError:
            pass
        resp_text = json.loads(resp.text)
        # 获取actuals，包含多个步骤信息的list
        actuals = self.jsonpath_value(resp_text, '$.data.actuals')
        # 如果存在branch_name及对应id，则获取step_id值
        if jsonpath(resp_text, f'$.data.branch[?(@.description=="{branch_name}")].id'):
            # 通过步骤名称获取需要选择的步骤id
            step_id = self.jsonpath_value(resp_text, f'$.data.branch[?(@.description=="{branch_name}")].id')
            # 更新actuals中step_id所在的toStepIds的list
            actual_steps = []
            # 如果待选择的步骤id，包含在toStepIds，则将toStepIds替换成已选择的
            for step in actuals:
                if step_id in step['toStepIds']:
                    step['toStepIds'] = [step_id]
                    actual_steps.append(step)
                else:
                    actual_steps.append(step)
            print(f"分支选择的actual_steps={actual_steps}")
            # 组成分支选择入参
            choose_body.update({"actual_steps": actual_steps, "choose_step_id": step_id})
            print(f'choose-branch-step={choose_body}')
            url = '/flowinstance/process/choose-branch-step'
            resp = self.send(url, method='post', payload=choose_body)
            return resp
        elif jsonpath(resp_text, f'$.data.branch')==[[]]:
            assert False, f'没有需要选择的步骤，请删除对应yml用例的branch_names中的【{branch_name}】参数!'
        else:
            assert False, f'【{branch_name}】步骤不是待选择的步骤名称，请调整对应yml用例的branch_names参数!'

    def decompress_gzip_string(self, compressed_string: str):
        try:
            # 将 Base64 编码的字符串解码为字节流
            compressed_bytes = base64.b64decode(compressed_string)

            # 创建一个 BytesIO 对象，用于处理压缩数据
            byte_stream = io.BytesIO(compressed_bytes)

            # 使用 gzip.GzipFile 解压缩数据
            with gzip.GzipFile(fileobj=byte_stream, mode='rb') as gzip_file:
                decompressed_bytes = gzip_file.read()

            # 将解压缩后的字节数据解码为 UTF-8 格式的字符串
            decompressed_string = decompressed_bytes.decode('utf-8')

            return decompressed_string

        except Exception as e:
            # 处理其他未知错误
            print(f"未知错误: {e}")
            return None

    def extract_and_setattr(self, resp):
        # 解压响应
        extract_content = self.decompress_gzip_string(resp.text)
        # 将 extract_content 转换为字节类型
        extract_content_bytes = extract_content.encode('utf-8')
        # 设置 _content 属性
        setattr(resp, '_content', extract_content_bytes)
        # 设置 _content_consumed 属性
        setattr(resp, '_content_consumed', True)
        # 如果需要设置 _text 属性，可以这样做，但通常不需要。因为 text 是基于 _content 解码得到的
        setattr(resp, '_text', extract_content)
    @allure.step('打开发起页面获取params和tplDate')
    def get_process_info_by_initiate(self, url_params='',full_url = ''):
        """
        通过接口获取发起流程的params和tplData
        Args:
            url_params: 获取流程信息的params参数，可以通过/flowinstance/process/create接口获得，如package_id=d33f7fef-a151-222b-f289-3a002480037e&business_id=&flow_template_id=7a7dbb9b-dc5b-f729-ac70-39fffc3779cf&flow_id=
            full_url: 完整的url，可以传入带http的地址，打开页面获取响应

        Returns:完整请求响应，包括响应头

        """
        if full_url != '':
            get_process_info_url = full_url
        else:
            get_process_info_url = f'/flowinstance/process/get-process-info?{url_params}'  # 创建流程时，获取流程信息url
        resp = self.send(url=get_process_info_url, method='get')
        # 解压缩并重置响应
        # self.extract_and_setattr(resp)
        self.csrf_token = self.jsonpath_value(resp.json(), "$.data.csrfToken")
        return resp

    @allure.step('打开审批页面获取params和tplDate')
    def get_process_info_by_approve(self, url_params='', full_url=''):
        """
        通过接口获取审批流程的params和tplData
        Args:
            url_params: 获取流程信息的params参数，可以通过/flowinstance/approval/index接口获得，如package_id=d33f7fef-a151-222b-f289-3a002480037e&business_id=&flow_template_id=7a7dbb9b-dc5b-f729-ac70-39fffc3779cf&flow_id=
            full_url: 完整的url，可以传入带http的地址，打开页面获取响应

        Returns: 完整请求响应，包括响应头

        """
        if full_url != '':
            get_process_info_url = full_url
        else:
            get_process_info_url = f'/flowinstance/approval/get-process-info?{url_params}'  # 创建流程时，获取流程信息url
        resp = self.send(url=get_process_info_url, method='get')
        # 解压缩并重置响应
        # self.extract_and_setattr(resp)
        self.csrf_token = self.jsonpath_value(resp.json(),"$.data.csrfToken")
        return resp

    def get_key_by_process_instance(self, open_instance_response, tpldata_or_params='', expr=''):
        """
        通过打开流程实例，获取响应后的有用的返回值：get_key_by_process_instance(process_instance_resp,'params','$.proc_id')['value']
        :param open_instance_response: 打开流程实例后的整个响应response，实际是get_process_info的返回
        :param tpldata_or_params: tpldata或params
        :param expr: jsonpath表达式
        :return: 字典：{'token':xxx,'value':xxx}或{'token':xxx,'params':xxx}或{'token':xxx,'tpldata':xxx}或{'token':xxx,'tpldata':xxx,'params': xxx}
        """
        params = jsonpath(open_instance_response.json(),'$.data.params')[0]
        tplData = jsonpath(open_instance_response.json(),'$.data.tplData')[0]
        # 因为params，tpldata，csrf_token多个接口都要用到，直接设置成对象变量
        self.params_json = params
        self.tpldata_json = tplData
        print(f"params={self.params_json}")
        print(f"tpldata ={self.tpldata_json}")
        return_dict = {'token': self.csrf_token}
        # 传入正确的tpldata，且传入jsonpath表达式，则返回返回token和对应的表达式的值，否则返回token+tpldata的字典
        if tpldata_or_params.lower() == 'tpldata':
            if expr != '':
                json_value = jsonpath(tplData, expr)
                value = json_value[0] if not isinstance(json_value, bool) else None
                return_dict.update({'value': value})
                return return_dict
            else:
                return_dict.update({'tpldata': tplData})
                return return_dict
        # 传入正确的params，且传入jsonpath表达式，则返回返回token和对应的表达式的值，否则返回token+params的字典
        elif tpldata_or_params.lower() == 'params':
            if expr != '':
                json_value = jsonpath(params, expr)
                value = json_value[0] if not isinstance(json_value, bool) else None
                return_dict.update({'value': value})
                return return_dict
            else:
                return_dict.update({'params': params})
                return return_dict
        # 否则返回token,params,tpldata的dict
        else:
            return_dict.update({'params': params, 'tpldata': tplData})
            return return_dict
